package com.gotokeep.keep.composition.source;

import android.graphics.SurfaceTexture;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.opengl.GLES11Ext;
import android.os.Handler;
import android.view.Surface;

import com.gotokeep.keep.composition.exception.IllegalMediaFormatException;
import com.gotokeep.keep.composition.exception.UnsupportedFormatException;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * @author xana/cuixianming
 * @version 1.0
 * @since 2018/5/1 22:43
 */
public class VideoMediaSource extends MediaSource implements VideoSource, SurfaceTexture.OnFrameAvailableListener {
    private String filePath;

    private SurfaceTexture outputTexture;
    private Surface outputSurface;

    private MediaExtractor extractor;
    private MediaFormat format;
    private MediaCodec decoder;
    private int trackIndex = -1;
    private int frameRate = 0;
    private int rotation = 0;
    private int keyFrameInterval = 0;
    private String mime = null;

    private long renderTimeUs = 0;
    private boolean isVideoEnd = false;
    private int inputIndex = -1;
    private int extractedDataSize = 0;
    private int extractedDataFlags = 0;

    private boolean textureFrameAvailable = false;

    public VideoMediaSource(String filePath, Handler eventHandler) {
        super(TYPE_VIDEO, Uri.parse(filePath).getLastPathSegment(), eventHandler);
        this.filePath = filePath;
    }

    @Override
    public void setOutputTexture(int textureType, int textureId) {
        if (textureType != GLES11Ext.GL_TEXTURE_EXTERNAL_OES) {
            if (eventHandler != null) {
                eventHandler.obtainMessage(MSG_ERROR, new IllegalArgumentException("VideoMediaSource must be set a " +
                        "GL_TEXTURE_EXTERNAL_OES texture")).sendToTarget();
            }
            return;
        }
        releaseTexture();
        outputTexture = new SurfaceTexture(textureId);
        outputSurface = new Surface(outputTexture);

        outputTexture.setOnFrameAvailableListener(this);
    }

    @Override
    public void updateTexImage() {
        if (textureFrameAvailable && outputTexture != null) {
            outputTexture.updateTexImage();
            textureFrameAvailable = false;
        }
    }

    @Override
    public boolean prepare() {
        if (outputSurface == null) {
            if (eventHandler != null) {
                eventHandler.obtainMessage(MSG_ERROR, new IllegalStateException("VideoMediaSource must be set a " +
                        "output texture before prepare"));
            }
            return false;
        }

        try {
            prepareExtractor();
            prepareDecoder();
            configure();
        } catch (IOException e) {
            if (eventHandler != null) {
                eventHandler.obtainMessage(MSG_ERROR, e).sendToTarget();
            }
            return false;
        }
        return true;
    }

    @Override
    public void prepareFrame(long presentationTimeUs) {
        if (extractor == null) {
            throw new IllegalStateException("MediaExtractor does not initialized.");
        }
        boolean decoded = false;
        while (!decoded) {
            inputIndex = decoder.dequeueInputBuffer(TIMEOUT_US);
            if (inputIndex >= 0) {
                ByteBuffer buffer = getInputBuffer(decoder, inputIndex);
                buffer.clear();
                extractedDataSize = extractor.readSampleData(buffer, 0);
                if (extractedDataSize < 0) {
                    isVideoEnd = true;
                } else {
                    long videoTimestamp = extractor.getSampleTime();
                    extractedDataFlags = extractor.getSampleFlags();
                    if (videoTimestamp < presentationTimeUs) {
                        decoder.queueInputBuffer(inputIndex, 0, extractedDataSize, videoTimestamp, extractedDataFlags);
                    } else {
                        decoded = true;
                        renderTimeUs = videoTimestamp;
                    }
                    if (!extractor.advance()) {
                        isVideoEnd = true;
                        decoded = true;
                    }
                }
            }
        }
    }

    @Override
    public void renderFrame() {
        if (isVideoEnd) {
            decoder.queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
        } else {
            decoder.queueInputBuffer(inputIndex, 0, extractedDataSize, renderTimeUs, extractedDataFlags);
        }
    }

    @Override
    public void reset() {
        extractor.seekTo(0, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
        decoder.stop();
        configure();
    }

    @Override
    public void release() {
        releaseDecoder();
        releaseExtractor();
        releaseTexture();
    }

    @Override
    public void seekTo(long timeUs) {
        extractor.seekTo(timeUs, MediaExtractor.SEEK_TO_PREVIOUS_SYNC);
    }

    @Override
    public boolean isEnd() {
        return isVideoEnd;
    }

    private void prepareExtractor() throws IOException {
        File sourceFile = new File(filePath);
        if (!sourceFile.exists()) {
            throw new FileNotFoundException("Composition media source file does not exists.");
        }

        extractor = new MediaExtractor();
        extractor.setDataSource(filePath);

        trackIndex = -1;
        for (int i = 0; i < extractor.getTrackCount(); i++) {
            format = extractor.getTrackFormat(i);
            mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("video/")) {
                trackIndex = i;
                break;
            }
        }
        if (trackIndex == -1) {
            throw new UnsupportedFormatException("Composition media source must contains at least one video track.");
        }

        width = format.getInteger(MediaFormat.KEY_WIDTH);
        height = format.getInteger(MediaFormat.KEY_HEIGHT);
        keyFrameInterval = format.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL);
        rotation = getRotation(filePath);
        frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
        durationUs = format.getLong(MediaFormat.KEY_DURATION);

        checkParameter("width", width, 0);
        checkParameter("height", height, 0);
        checkParameter("keyFrameInterval", keyFrameInterval, 0);
        checkParameter("frameRate", frameRate, 0);
        checkParameter("duration", durationUs, 0L);
        extractor.selectTrack(trackIndex);
    }

    private void prepareDecoder() throws IOException {
        decoder = MediaCodec.createDecoderByType(mime);
    }

    private void configure() {
        if (decoder != null) {
            decoder.configure(format, outputSurface, null, 0);
            decoder.start();
        }
    }

    private void releaseExtractor() {
        if (extractor != null) {
            extractor.release();
            extractor = null;
        }
    }

    private void releaseDecoder() {
        if (decoder != null) {
            decoder.stop();
            decoder.release();
            decoder = null;
        }
    }

    private void releaseTexture() {
        if (outputSurface != null) {
            outputSurface.release();
            outputSurface = null;
        }
        if (outputTexture != null) {
            outputTexture.release();
            outputTexture = null;
        }
    }

    private <T extends Comparable<T>> void checkParameter(String name, T value, T target) throws
            IllegalMediaFormatException {
        if (value.compareTo(target) < 0) {
            throw new IllegalMediaFormatException(String.format("Invalid parameter \"%s\": %s", name, value.toString()));
        }
    }

    private int getRotation(String path) {
        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
        try {
            retriever.setDataSource(path);
            String rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
            return Integer.parseInt(rotation);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        if (eventHandler != null) {
            eventHandler.sendEmptyMessage(MSG_FRAME_AVALIABLE);
        }
        textureFrameAvailable = true;
    }
}
